Skip to content

feat(form-core): 5-10x faster makePathArray#2152

Open
GiacoCorsiglia wants to merge 10 commits into
TanStack:mainfrom
GiacoCorsiglia:faster-make-path-array
Open

feat(form-core): 5-10x faster makePathArray#2152
GiacoCorsiglia wants to merge 10 commits into
TanStack:mainfrom
GiacoCorsiglia:faster-make-path-array

Conversation

@GiacoCorsiglia
Copy link
Copy Markdown

@GiacoCorsiglia GiacoCorsiglia commented May 8, 2026

🎯 Changes

NB: I used Claude Code when doing this work (but not to write this description).

In #2150 I noted that mounting <form.Field> is slow because it exhibits O(N^2) complexity, where N is the number of fields in the form. This PR does not fix the O(N^2) behavior, but when profiling it, I noticed that makePathArray is a super hot path.

On main, makePathArray includes a bunch of regex string munging. I rewrote it as a single-pass for loop. This produces 5–10x speed up in microbenchmarks, and what appears to be ~2x faster <form.Field> mounting/unmounting.

I also wrote additional tests for makePathArray to try to capture its edge case behavior (what I think are malformed inputs—things not allowed by DeepKeys<T>). I did my best to maintain backwards compatibility, but you will see there is one BC-break I identified:

// Old behavior:
expect(makePathArray('a]b')).toEqual(['ab'])
// New behavior:
expect(makePathArray('a]b')).toEqual(['a', 'b'])

I may have missed some edge cases not covered by my new tests. It's possible to match the old behavior more closely. Claude's original implementation was much more complex and handled more edge cases, but I opted to simplify so that the code was easier to understand. Happy to adjust.

Benchmarks

This branch includes benchmark files I would not expect to be merged, but wanted to include temporarily so others can validate my results (run on my M4 Pro MackBook Pro).

Here's the output for the new utils.bench.ts:

 BENCH  Summary

   @tanstack/form-core  old - tests/utils.bench.ts > array input (fast path, no parsing)
    1.10x faster than new

   @tanstack/form-core  new - tests/utils.bench.ts > simple key (no nesting)
    7.56x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > uuid key
    4.78x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > dot notation
    5.90x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > mixed dot and bracket notation
    11.81x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > deeply nested mixed path
    11.61x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > numeric string with leading zeros (kept as string)
    9.95x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > numeric string (converted to number)
    9.67x faster than old

In addition, I (well, Claude) wrote field-render-perf.test.tsx, which is an automated version of the reproduction I put together for #2150.

Results with the old makePathArray():

iterations=5 warmup=1 env=jsdom (median of 5)
    N      mount    unmount      total        total min..max
  100     17.9ms     18.5ms     36.3ms          35.3..45.8ms
  500    185.1ms    419.9ms    601.5ms        599.8..612.1ms
 1000    691.2ms   1897.7ms   2549.5ms      2457.9..2677.3ms
 2000   2709.0ms   8351.3ms  11060.6ms    10882.0..11462.1ms

Results with the new makePathArray():

iterations=5 warmup=1 env=jsdom (median of 5)
    N      mount    unmount      total        total min..max
  100     13.2ms      7.6ms     20.9ms          18.1..26.8ms
  500     86.0ms    130.4ms    218.2ms        213.2..232.6ms
 1000    293.7ms    719.7ms   1015.8ms       929.1..1081.8ms
 2000   1190.4ms   3499.1ms   4705.3ms      4456.6..4951.0ms

~2x faster!

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Refactor

    • Improved path-parsing implementation for better performance and robustness
  • Tests

    • Added benchmark tests for path parsing and updated unit tests to reflect new parsing behavior
    • Added a skipped-by-default performance test suite for field render/mount/unmount timing
  • Chores

    • Added bench scripts and test config to enable benchmarks
    • Ignoring CPU profile files via .gitignore
    • Added a changeset documenting the performance improvement

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR rewrites makePathArray to a single-pass character-code parser, updates unit tests, adds vitest benchmarks and npm scripts, introduces a skipped field-render perf test with optional V8 profiling, and ignores generated .cpuprofile files.

Changes

Path Parsing Optimization and Benchmarking

Layer / File(s) Summary
Core Algorithm: Character-Code Parser
packages/form-core/src/utils.ts
makePathArray reimplemented as single-pass character-code parser with explicit segment handling, digit-to-number conversion, and malformed-input backward compatibility via phantom boundary logic.
Unit Test Coverage
packages/form-core/tests/utils.spec.ts
Tests expanded for numeric handling (lone "0" becomes 0, huge digit strings remain strings, leading zeros preserved mid-path), array copy semantics, type validation, and malformed path corner cases like a]b splitting to ['a','b'].
Benchmarking Infrastructure
packages/form-core/tests/utils.bench.ts, packages/form-core/vite.config.ts, packages/form-core/package.json
Vitest benchmark file captures prior regex-based makePathArrayOld, defines representative test cases, runs side-by-side bench comparisons; Vite config includes benchmark files; package.json adds test:bench and test:bench:dev scripts.
Field Rendering Performance Test
packages/react-form/tests/field-render-perf.test.tsx
New skipped-by-default performance test measures mount/unmount cost across field counts with timing summaries; optional V8 CPU profiling via Node Inspector writes .cpuprofile artifacts when enabled by environment variables.
Build Configuration
.gitignore, .changeset/*
CPU profile files (*.cpuprofile) added to ignore list; a changeset entry documents a patch release for @tanstack/form-core.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through bytes and split each dot,
I watched the parser leap and plot.
Benchmarks hum a steady beat,
Profiles saved for curious feet,
Fields mount light — the code's complete! 🎩

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: rewriting makePathArray for performance. It is specific, concise, and reflects the core contribution of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description comprehensively covers changes, motivation, performance benchmarks, testing, and checklist items with clear detail about the makePathArray rewrite and its impact.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
Comment thread packages/form-core/vite.config.ts
Comment thread packages/form-core/package.json
Comment thread packages/form-core/tests/utils.bench.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/form-core/src/utils.ts (1)

227-230: 💤 Low value

Incomplete comment — "for these because." is a sentence fragment.

✏️ Suggested fix
-  // for these because.
+  // for these, because the old regex pipeline always produced at least one empty string
+  // from the split even when the input was only separator/meta characters.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/form-core/src/utils.ts` around lines 227 - 230, The comment above
the conditional that checks "if (!result.length) result.push('')" is a fragment;
update it to a complete sentence that explains why the old implementation
returned [''] (i.e., when the input contained only phantom characters like ']',
'[]', '[]]' producing no segments, the old behavior produced a single empty
segment to represent an empty path). Edit the comment near the result variable
and the conditional so it reads as a full explanatory sentence referencing the
phantom-char inputs and the intended empty-segment fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/form-core/src/utils.ts`:
- Around line 188-198: The current logic in the segment parsing branch
(variables: treatAsNumber, allDigits, segLen, str.charCodeAt(segStart) ===
CC_ZERO, parseInt call and result.push) unconditionally pushes parseInt(...)
which loses precision for integers > Number.MAX_SAFE_INTEGER; add a round-trip
guard: when treatAsNumber is true, parse the segment into a number (e.g., parsed
= parseInt(str.slice(segStart, i), 10)) but only push parsed if String(parsed)
=== the original segment string; otherwise push the original slice
(str.slice(segStart, i)) so large integer strings are preserved as strings.

In `@packages/form-core/tests/utils.bench.ts`:
- Around line 4-6: The file contains a temporary snapshot and an inline legacy
function makePathArrayOld plus a "remove this" comment and paired benches that
must not land on main; remove the makePathArrayOld function and the accompanying
comment and any benchmark comparisons that reference it (e.g., paired benches)
and restructure the test to benchmark only the current live implementation (or
delete the whole bench file if benchmarking was never intended to be shipped);
ensure any helper regex like reLineOfOnlyDigits and tests only used by the old
implementation are also removed or repurposed so the file contains solely the
intended, production benchmark code.

In `@packages/react-form/tests/field-render-perf.test.tsx`:
- Line 29: This test file is marked for removal ("NOTE: This file is intended to
be removed before merge.") and should not land in main; remove the temporary
perf test from the PR (or move it out of the commit into a dedicated draft
branch) and instead open a follow-up issue or add a CI-safe permanent test in
the proper tests directory so the throwaway file does not get merged.
- Around line 178-184: The test uses import.meta.dirname when building mountPath
and unmountPath which fails on Node <21.2.0; add a fallback that computes a
dirname from fileURLToPath(import.meta.url) and use that variable instead of
import.meta.dirname. Specifically, near the top of the test module define a
const (e.g., testDir or dirname) that sets dirname = import.meta.dirname ??
path.dirname(fileURLToPath(import.meta.url)) and then update the join calls that
create mountPath and unmountPath (and any other uses of import.meta.dirname) to
use that dirname; reference functions/symbols: mountPath, unmountPath,
PROFILE_N, join, import.meta.dirname, import.meta.url, fileURLToPath.

---

Nitpick comments:
In `@packages/form-core/src/utils.ts`:
- Around line 227-230: The comment above the conditional that checks "if
(!result.length) result.push('')" is a fragment; update it to a complete
sentence that explains why the old implementation returned [''] (i.e., when the
input contained only phantom characters like ']', '[]', '[]]' producing no
segments, the old behavior produced a single empty segment to represent an empty
path). Edit the comment near the result variable and the conditional so it reads
as a full explanatory sentence referencing the phantom-char inputs and the
intended empty-segment fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2789ad5-a68f-4f50-a809-80d8c5d11698

📥 Commits

Reviewing files that changed from the base of the PR and between cab571a and 015ea3a.

📒 Files selected for processing (7)
  • .gitignore
  • packages/form-core/package.json
  • packages/form-core/src/utils.ts
  • packages/form-core/tests/utils.bench.ts
  • packages/form-core/tests/utils.spec.ts
  • packages/form-core/vite.config.ts
  • packages/react-form/tests/field-render-perf.test.tsx

Comment thread packages/form-core/src/utils.ts
Comment thread packages/form-core/tests/utils.bench.ts Outdated
Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
@crutchcorn
Copy link
Copy Markdown
Member

This is awesome, thank you!

Consider the new code "LGTM"'d. Let's go ahead and remove the old code, remove benchmarking, and generally clean up the PR to land in main

Let me know what you need from us!

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 10, 2026

View your CI Pipeline Execution ↗ for commit 475b5c7

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 21s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 31s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-10 19:46:39 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 10, 2026

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@2152

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@2152

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@2152

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@2152

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@2152

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@2152

@tanstack/react-form-nextjs

npm i https://pkg.pr.new/@tanstack/react-form-nextjs@2152

@tanstack/react-form-remix

npm i https://pkg.pr.new/@tanstack/react-form-remix@2152

@tanstack/react-form-start

npm i https://pkg.pr.new/@tanstack/react-form-start@2152

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@2152

@tanstack/solid-form-devtools

npm i https://pkg.pr.new/@tanstack/solid-form-devtools@2152

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@2152

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@2152

commit: 475b5c7

@sentry
Copy link
Copy Markdown

sentry Bot commented May 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.46%. Comparing base (6892ed0) to head (475b5c7).
⚠️ Report is 200 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2152      +/-   ##
==========================================
+ Coverage   90.35%   90.46%   +0.10%     
==========================================
  Files          38       49      +11     
  Lines        1752     2065     +313     
  Branches      444      546     +102     
==========================================
+ Hits         1583     1868     +285     
- Misses        149      177      +28     
  Partials       20       20              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread .gitignore
stats-hydration.json
stats.json
stats.html
*.cpuprofile
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured I might as well leave this

@GiacoCorsiglia
Copy link
Copy Markdown
Author

@crutchcorn Amazing! I removed all the benchmarks and cleaned up the comments in the new tests, so I think it's ready to be merged now. But let me know if you need anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants